agora inbox for pgsql-hackers@postgresql.org  
help / color / mirror / Atom feed
[PATCH] Allow CREATE INDEX CONCURRENTLY on partitioned table
659+ messages / 2 participants
[nested] [flat]

* [PATCH] Allow CREATE INDEX CONCURRENTLY on partitioned table
@ 2020-06-06 22:42 Justin Pryzby <pryzbyj@telsasoft.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Justin Pryzby @ 2020-06-06 22:42 UTC (permalink / raw)

Note, this effectively reverts 050098b14, so take care to not reintroduce the
bug it fixed.
---
 doc/src/sgml/ddl.sgml                  |   4 +-
 doc/src/sgml/ref/create_index.sgml     |   9 --
 src/backend/commands/indexcmds.c       | 214 +++++++++++++++++++------
 src/include/catalog/index.h            |   1 +
 src/test/regress/expected/indexing.out | 136 +++++++++++++++-
 src/test/regress/sql/indexing.sql      |  26 ++-
 6 files changed, 320 insertions(+), 70 deletions(-)

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 03c01937094..fd56e21ef49 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -4131,9 +4131,7 @@ ALTER TABLE measurement ATTACH PARTITION measurement_y2008m02
      so that they are applied automatically to the entire hierarchy.
      This is very
      convenient, as not only will the existing partitions become indexed, but
-     also any partitions that are created in the future will.  One limitation is
-     that it's not possible to use the <literal>CONCURRENTLY</literal>
-     qualifier when creating such a partitioned index.  To avoid long lock
+     also any partitions that are created in the future will.  To avoid long lock
      times, it is possible to use <command>CREATE INDEX ON ONLY</command>
      the partitioned table; such an index is marked invalid, and the partitions
      do not get the index applied automatically.  The indexes on partitions can
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 40986aa502f..fc8cda655f0 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -692,15 +692,6 @@ Indexes:
     cannot.
    </para>
 
-   <para>
-    Concurrent builds for indexes on partitioned tables are currently not
-    supported.  However, you may concurrently build the index on each
-    partition individually and then finally create the partitioned index
-    non-concurrently in order to reduce the time where writes to the
-    partitioned table will be locked out.  In this case, building the
-    partitioned index is a metadata only operation.
-   </para>
-
   </refsect2>
  </refsect1>
 
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 91cee27743d..bb98e745267 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -71,6 +71,7 @@
 
 
 /* non-export function prototypes */
+static void reindex_invalid_child_indexes(Oid indexRelationId);
 static bool CompareOpclassOptions(Datum *opts1, Datum *opts2, int natts);
 static void CheckPredicate(Expr *predicate);
 static void ComputeIndexAttrs(IndexInfo *indexInfo,
@@ -104,7 +105,9 @@ static void reindex_error_callback(void *arg);
 static void ReindexPartitions(Oid relid, ReindexParams *params,
 							  bool isTopLevel);
 static void ReindexMultipleInternal(List *relids,
-									ReindexParams *params);
+									ReindexParams *params,
+									Oid parent,
+									int npart);
 static bool ReindexRelationConcurrently(Oid relationOid,
 										ReindexParams *params);
 static void update_relispartition(Oid relationId, bool newval);
@@ -697,17 +700,6 @@ DefineIndex(Oid relationId,
 	partitioned = rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE;
 	if (partitioned)
 	{
-		/*
-		 * Note: we check 'stmt->concurrent' rather than 'concurrent', so that
-		 * the error is thrown also for temporary tables.  Seems better to be
-		 * consistent, even though we could do it on temporary table because
-		 * we're not actually doing it concurrently.
-		 */
-		if (stmt->concurrent)
-			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("cannot create index on partitioned table \"%s\" concurrently",
-							RelationGetRelationName(rel))));
 		if (stmt->excludeOpNames)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
@@ -1147,6 +1139,11 @@ DefineIndex(Oid relationId,
 		if (pd->nparts != 0)
 			flags |= INDEX_CREATE_INVALID;
 	}
+	else if (concurrent && OidIsValid(parentIndexId))
+	{
+		/* If concurrent, initially build index partitions as "invalid" */
+		flags |= INDEX_CREATE_INVALID;
+	}
 
 	if (stmt->deferrable)
 		constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
@@ -1212,17 +1209,30 @@ DefineIndex(Oid relationId,
 		partdesc = RelationGetPartitionDesc(rel, true);
 		if ((!stmt->relation || stmt->relation->inh) && partdesc->nparts > 0)
 		{
+			/*
+			 * Need to close the relation before recursing into children, so
+			 * copy needed data into a longlived context.
+			 */
+
+			MemoryContext	ind_context = AllocSetContextCreate(PortalContext, "CREATE INDEX",
+					ALLOCSET_DEFAULT_SIZES);
+			MemoryContext	oldcontext = MemoryContextSwitchTo(ind_context);
 			int			nparts = partdesc->nparts;
 			Oid		   *part_oids = palloc_array(Oid, nparts);
 			bool		invalidate_parent = false;
 			Relation	parentIndex;
 			TupleDesc	parentDesc;
+			char		*relname;
 
 			pgstat_progress_update_param(PROGRESS_CREATEIDX_PARTITIONS_TOTAL,
 										 nparts);
 
 			/* Make a local copy of partdesc->oids[], just for safety */
 			memcpy(part_oids, partdesc->oids, sizeof(Oid) * nparts);
+			parentDesc = CreateTupleDescCopy(RelationGetDescr(rel));
+			relname = pstrdup(RelationGetRelationName(rel));
+			table_close(rel, NoLock);
+			MemoryContextSwitchTo(oldcontext);
 
 			/*
 			 * We'll need an IndexInfo describing the parent index.  The one
@@ -1235,8 +1245,6 @@ DefineIndex(Oid relationId,
 			parentIndex = index_open(indexRelationId, lockmode);
 			indexInfo = BuildIndexInfo(parentIndex);
 
-			parentDesc = RelationGetDescr(rel);
-
 			/*
 			 * For each partition, scan all existing indexes; if one matches
 			 * our index definition and is not already attached to some other
@@ -1276,9 +1284,9 @@ DefineIndex(Oid relationId,
 						ereport(ERROR,
 								(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 								 errmsg("cannot create unique index on partitioned table \"%s\"",
-										RelationGetRelationName(rel)),
+										relname),
 								 errdetail("Table \"%s\" contains partitions that are foreign tables.",
-										   RelationGetRelationName(rel))));
+										   relname)));
 
 					AtEOXact_GUC(false, child_save_nestlevel);
 					SetUserIdAndSecContext(child_save_userid,
@@ -1364,10 +1372,14 @@ DefineIndex(Oid relationId,
 				 */
 				if (!found)
 				{
-					IndexStmt  *childStmt = copyObject(stmt);
+					IndexStmt  *childStmt;
 					bool		found_whole_row;
 					ListCell   *lc;
 
+					oldcontext = MemoryContextSwitchTo(ind_context);
+					childStmt = copyObject(stmt);
+					MemoryContextSwitchTo(oldcontext);
+
 					/*
 					 * We can't use the same index name for the child index,
 					 * so clear idxname to let the recursive invocation choose
@@ -1426,14 +1438,24 @@ DefineIndex(Oid relationId,
 								createdConstraintId,
 								is_alter_table, check_rights, check_not_in_use,
 								skip_build, quiet);
+
 					SetUserIdAndSecContext(child_save_userid,
 										   child_save_sec_context);
+					if (concurrent)
+					{
+						PopActiveSnapshot();
+						PushActiveSnapshot(GetTransactionSnapshot());
+						invalidate_parent = true;
+					}
 				}
 
-				pgstat_progress_update_param(PROGRESS_CREATEIDX_PARTITIONS_DONE,
-											 i + 1);
+				/* For concurrent build, this is a catalog-only stage */
+				if (!concurrent)
+					pgstat_progress_update_param(PROGRESS_CREATEIDX_PARTITIONS_DONE,
+												 i + 1);
 				free_attrmap(attmap);
 			}
+			pfree(relname);
 
 			index_close(parentIndex, lockmode);
 
@@ -1443,46 +1465,40 @@ DefineIndex(Oid relationId,
 			 * invalid, this is incorrect, so update our row to invalid too.
 			 */
 			if (invalidate_parent)
-			{
-				Relation	pg_index = table_open(IndexRelationId, RowExclusiveLock);
-				HeapTuple	tup,
-							newtup;
-
-				tup = SearchSysCache1(INDEXRELID,
-									  ObjectIdGetDatum(indexRelationId));
-				if (!HeapTupleIsValid(tup))
-					elog(ERROR, "cache lookup failed for index %u",
-						 indexRelationId);
-				newtup = heap_copytuple(tup);
-				((Form_pg_index) GETSTRUCT(newtup))->indisvalid = false;
-				CatalogTupleUpdate(pg_index, &tup->t_self, newtup);
-				ReleaseSysCache(tup);
-				table_close(pg_index, RowExclusiveLock);
-				heap_freetuple(newtup);
-			}
-		}
+				index_set_state_flags(indexRelationId, INDEX_DROP_CLEAR_VALID);
+		} else
+			table_close(rel, NoLock);
 
 		/*
 		 * Indexes on partitioned tables are not themselves built, so we're
 		 * done here.
 		 */
+
 		AtEOXact_GUC(false, root_save_nestlevel);
 		SetUserIdAndSecContext(root_save_userid, root_save_sec_context);
-		table_close(rel, NoLock);
+
 		if (!OidIsValid(parentIndexId))
+		{
+			if (concurrent)
+				reindex_invalid_child_indexes(indexRelationId);
+
 			pgstat_progress_end_command();
+		}
+
 		return address;
 	}
 
 	AtEOXact_GUC(false, root_save_nestlevel);
 	SetUserIdAndSecContext(root_save_userid, root_save_sec_context);
 
-	if (!concurrent)
+	if (!concurrent || OidIsValid(parentIndexId))
 	{
-		/* Close the heap and we're done, in the non-concurrent case */
-		table_close(rel, NoLock);
+		/*
+		 * We're done if this is the top-level index,
+		 * or the catalog-only phase of a partition built concurrently
+		 */
 
-		/* If this is the top-level index, we're done. */
+		table_close(rel, NoLock);
 		if (!OidIsValid(parentIndexId))
 			pgstat_progress_end_command();
 
@@ -1695,6 +1711,70 @@ DefineIndex(Oid relationId,
 	return address;
 }
 
+/* Reindex invalid child indexes created earlier */
+static void
+reindex_invalid_child_indexes(Oid indexRelationId)
+{
+	ListCell *lc;
+	List	*parentindexes = NIL;
+	int	npart = 1;
+	ReindexParams params = {
+		.options = REINDEXOPT_CONCURRENTLY | REINDEXOPT_REPORT_CREATE_PART
+	};
+
+	MemoryContext	ind_context = AllocSetContextCreate(PortalContext, "CREATE INDEX",
+			ALLOCSET_DEFAULT_SIZES);
+	MemoryContext	oldcontext;
+	List		*childs = find_all_inheritors(indexRelationId, ShareLock, NULL);
+	List		*partitions = NIL;
+
+	PreventInTransactionBlock(true, "REINDEX INDEX");
+
+	foreach (lc, childs)
+	{
+		Oid			partoid = lfirst_oid(lc);
+		char		partkind = get_rel_relkind(partoid);
+
+		if (partkind == RELKIND_PARTITIONED_INDEX)
+		{
+			oldcontext = MemoryContextSwitchTo(ind_context);
+			parentindexes = lappend_oid(parentindexes, partoid);
+			MemoryContextSwitchTo(oldcontext);
+		}
+
+		// npart++ ?
+		if (!RELKIND_HAS_STORAGE(partkind) || get_index_isvalid(partoid))
+			continue;
+
+		/* Save partition OID */
+		oldcontext = MemoryContextSwitchTo(ind_context);
+		partitions = lappend_oid(partitions, partoid);
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	/*
+	 * Process each partition listed in a separate transaction.  Note that
+	 * this commits and then starts a new transaction immediately.
+	 * XXX: since this is done in 2*N transactions, it could just as well
+	 * call ReindexRelationConcurrently directly
+	 */
+	ReindexMultipleInternal(partitions, &params, indexRelationId, npart);
+
+	/*
+	 * CIC needs to mark a partitioned index as VALID, which itself
+	 * requires setting READY, which is unset for CIC (even though it's
+	 * meaningless for an index without storage).  This must be done only
+	 * while holding a lock which precludes adding partitions.
+	 * See also: validatePartitionedIndex().
+	 */
+	foreach (lc, parentindexes)
+	{
+		Oid	partoid = lfirst_oid(lc);
+		index_set_state_flags(partoid, INDEX_CREATE_SET_READY);
+		CommandCounterIncrement();
+		index_set_state_flags(partoid, INDEX_CREATE_SET_VALID);
+	}
+}
 
 /*
  * CheckMutability
@@ -3084,7 +3164,7 @@ ReindexMultipleTables(const char *objectName, ReindexObjectType objectKind,
 	 * Process each relation listed in a separate transaction.  Note that this
 	 * commits and then starts a new transaction immediately.
 	 */
-	ReindexMultipleInternal(relids, params);
+	ReindexMultipleInternal(relids, params, InvalidOid, 0);
 
 	MemoryContextDelete(private_context);
 }
@@ -3120,6 +3200,7 @@ ReindexPartitions(Oid relid, ReindexParams *params, bool isTopLevel)
 	char		relkind = get_rel_relkind(relid);
 	char	   *relname = get_rel_name(relid);
 	char	   *relnamespace = get_namespace_name(get_rel_namespace(relid));
+	int			npart = 1;
 	MemoryContext reindex_context;
 	List	   *inhoids;
 	ListCell   *lc;
@@ -3190,7 +3271,7 @@ ReindexPartitions(Oid relid, ReindexParams *params, bool isTopLevel)
 	 * Process each partition listed in a separate transaction.  Note that
 	 * this commits and then starts a new transaction immediately.
 	 */
-	ReindexMultipleInternal(partitions, params);
+	ReindexMultipleInternal(partitions, params, relid, npart);
 
 	/*
 	 * Clean up working storage --- note we must do this after
@@ -3208,7 +3289,7 @@ ReindexPartitions(Oid relid, ReindexParams *params, bool isTopLevel)
  * and starts a new transaction when finished.
  */
 static void
-ReindexMultipleInternal(List *relids, ReindexParams *params)
+ReindexMultipleInternal(List *relids, ReindexParams *params, Oid parent, int npart)
 {
 	ListCell   *l;
 
@@ -3302,6 +3383,29 @@ ReindexMultipleInternal(List *relids, ReindexParams *params)
 		}
 
 		CommitTransactionCommand();
+
+		if (params->options & REINDEXOPT_REPORT_CREATE_PART)
+		{
+			/*
+			 * Update pgstat progress report to indicate that create index on
+			 * partition was finished.
+			 */
+			const int   progress_cols[] = {
+				PROGRESS_CREATEIDX_COMMAND,
+				PROGRESS_CREATEIDX_INDEX_OID,
+				PROGRESS_CREATEIDX_PHASE,
+				PROGRESS_CREATEIDX_PARTITIONS_DONE
+			};
+			const int64 progress_vals[] = {
+				PROGRESS_CREATEIDX_COMMAND_CREATE_CONCURRENTLY,
+				parent,
+				PROGRESS_CREATEIDX_PHASE_VALIDATE_IDXSCAN,
+				npart++
+			};
+
+			pgstat_progress_update_multi_param(4, progress_cols, progress_vals);
+		}
+
 	}
 
 	StartTransactionCommand();
@@ -3697,7 +3801,9 @@ ReindexRelationConcurrently(Oid relationOid, ReindexParams *params)
 		if (indexRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
 			elog(ERROR, "cannot reindex a temporary table concurrently");
 
-		pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX,
+		/* Don't overwrite CREATE INDEX command */
+		if (!(params->options & REINDEXOPT_REPORT_CREATE_PART))
+			pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX,
 									  idx->tableId);
 
 		progress_vals[0] = PROGRESS_CREATEIDX_COMMAND_REINDEX_CONCURRENTLY;
@@ -3857,9 +3963,11 @@ ReindexRelationConcurrently(Oid relationOid, ReindexParams *params)
 
 		/*
 		 * Update progress for the index to build, with the correct parent
-		 * table involved.
+		 * table involved.  Don't overwrite CREATE INDEX command.
 		 */
-		pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX, newidx->tableId);
+		if (!(params->options & REINDEXOPT_REPORT_CREATE_PART))
+			pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX, newidx->tableId);
+
 		progress_vals[0] = PROGRESS_CREATEIDX_COMMAND_REINDEX_CONCURRENTLY;
 		progress_vals[1] = PROGRESS_CREATEIDX_PHASE_BUILD;
 		progress_vals[2] = newidx->indexId;
@@ -3921,10 +4029,12 @@ ReindexRelationConcurrently(Oid relationOid, ReindexParams *params)
 
 		/*
 		 * Update progress for the index to build, with the correct parent
-		 * table involved.
+		 * table involved. Don't overwrite CREATE INDEX command.
 		 */
-		pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX,
+		if (!(params->options & REINDEXOPT_REPORT_CREATE_PART))
+			pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX,
 									  newidx->tableId);
+
 		progress_vals[0] = PROGRESS_CREATEIDX_COMMAND_REINDEX_CONCURRENTLY;
 		progress_vals[1] = PROGRESS_CREATEIDX_PHASE_VALIDATE_IDXSCAN;
 		progress_vals[2] = newidx->indexId;
@@ -4159,7 +4269,9 @@ ReindexRelationConcurrently(Oid relationOid, ReindexParams *params)
 
 	MemoryContextDelete(private_context);
 
-	pgstat_progress_end_command();
+	/* Don't overwrite CREATE INDEX command. */
+	if (!(params->options & REINDEXOPT_REPORT_CREATE_PART))
+		pgstat_progress_end_command();
 
 	return true;
 }
diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h
index 91c28868d47..4142894c230 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -42,6 +42,7 @@ typedef struct ReindexParams
 #define REINDEXOPT_REPORT_PROGRESS 0x02 /* report pgstat progress */
 #define REINDEXOPT_MISSING_OK 	0x04	/* skip missing relations */
 #define REINDEXOPT_CONCURRENTLY	0x08	/* concurrent mode */
+#define REINDEXOPT_REPORT_CREATE_PART	0x10	/* report that index was created for partition */
 
 /* state info for validate_index bulkdelete callback */
 typedef struct ValidateIndexState
diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out
index 1bdd430f063..6b2320244b8 100644
--- a/src/test/regress/expected/indexing.out
+++ b/src/test/regress/expected/indexing.out
@@ -50,11 +50,139 @@ select relname, relkind, relhassubclass, inhparent::regclass
 (8 rows)
 
 drop table idxpart;
--- Some unsupported features
+-- CIC on partitioned table
 create table idxpart (a int, b int, c text) partition by range (a);
-create table idxpart1 partition of idxpart for values from (0) to (10);
-create index concurrently on idxpart (a);
-ERROR:  cannot create index on partitioned table "idxpart" concurrently
+create table idxpart1 partition of idxpart for values from (0) to (10) partition by range(a);
+create table idxpart11 partition of idxpart1 for values from (0) to (10) partition by range(a);
+create table idxpart111 partition of idxpart11 default partition by range(a);
+create table idxpart1111 partition of idxpart111 default partition by range(a);
+create table idxpart2 partition of idxpart for values from (10) to (20);
+create table idxpart3 partition of idxpart for values from (30) to (40) partition by range(a);
+create table idxpart31 partition of idxpart3 default;
+insert into idxpart2 values(10),(10); -- not unique
+create index concurrently on idxpart (a); -- partitioned
+create index concurrently on idxpart1 (a); -- partitioned and partition
+create index concurrently on idxpart11 (a); -- partitioned and partition, with no leaves
+create index concurrently on idxpart2 (a); -- leaf
+create unique index concurrently on idxpart (a); -- partitioned, unique failure
+ERROR:  could not create unique index "idxpart2_a_idx2_ccnew"
+DETAIL:  Key (a)=(10) is duplicated.
+\d idxpart
+        Partitioned table "public.idxpart"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+Partition key: RANGE (a)
+Indexes:
+    "idxpart_a_idx" btree (a)
+    "idxpart_a_idx1" UNIQUE, btree (a) INVALID
+Number of partitions: 3 (Use \d+ to list them.)
+
+\d idxpart1
+        Partitioned table "public.idxpart1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+Partition of: idxpart FOR VALUES FROM (0) TO (10)
+Partition key: RANGE (a)
+Indexes:
+    "idxpart1_a_idx" btree (a)
+    "idxpart1_a_idx1" btree (a)
+    "idxpart1_a_idx2" UNIQUE, btree (a) INVALID
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d idxpart11
+       Partitioned table "public.idxpart11"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+Partition of: idxpart1 FOR VALUES FROM (0) TO (10)
+Partition key: RANGE (a)
+Indexes:
+    "idxpart11_a_idx" btree (a)
+    "idxpart11_a_idx1" btree (a)
+    "idxpart11_a_idx2" btree (a)
+    "idxpart11_a_idx3" UNIQUE, btree (a) INVALID
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d idxpart111
+       Partitioned table "public.idxpart111"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+Partition of: idxpart11 DEFAULT
+Partition key: RANGE (a)
+Indexes:
+    "idxpart111_a_idx" btree (a)
+    "idxpart111_a_idx1" btree (a)
+    "idxpart111_a_idx2" btree (a)
+    "idxpart111_a_idx3" UNIQUE, btree (a) INVALID
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d idxpart1111
+      Partitioned table "public.idxpart1111"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+Partition of: idxpart111 DEFAULT
+Partition key: RANGE (a)
+Indexes:
+    "idxpart1111_a_idx" btree (a)
+    "idxpart1111_a_idx1" btree (a)
+    "idxpart1111_a_idx2" btree (a)
+    "idxpart1111_a_idx3" UNIQUE, btree (a) INVALID
+Number of partitions: 0
+
+\d idxpart2
+              Table "public.idxpart2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+Partition of: idxpart FOR VALUES FROM (10) TO (20)
+Indexes:
+    "idxpart2_a_idx" btree (a)
+    "idxpart2_a_idx1" btree (a)
+    "idxpart2_a_idx2" UNIQUE, btree (a) INVALID
+    "idxpart2_a_idx2_ccnew" UNIQUE, btree (a) INVALID
+
+\d idxpart3
+        Partitioned table "public.idxpart3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+Partition of: idxpart FOR VALUES FROM (30) TO (40)
+Partition key: RANGE (a)
+Indexes:
+    "idxpart3_a_idx" btree (a)
+    "idxpart3_a_idx1" UNIQUE, btree (a) INVALID
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d idxpart31
+             Table "public.idxpart31"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+ c      | text    |           |          | 
+Partition of: idxpart3 DEFAULT
+Indexes:
+    "idxpart31_a_idx" btree (a)
+    "idxpart31_a_idx1" UNIQUE, btree (a) INVALID
+
 drop table idxpart;
 -- Verify bugfix with query on indexed partitioned table with no partitions
 -- https://postgr.es/m/20180124162006.pmapfiznhgngwtjf@alvherre.pgsql
diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql
index 429120e7104..14e0513386c 100644
--- a/src/test/regress/sql/indexing.sql
+++ b/src/test/regress/sql/indexing.sql
@@ -29,10 +29,30 @@ select relname, relkind, relhassubclass, inhparent::regclass
 	where relname like 'idxpart%' order by relname;
 drop table idxpart;
 
--- Some unsupported features
+-- CIC on partitioned table
 create table idxpart (a int, b int, c text) partition by range (a);
-create table idxpart1 partition of idxpart for values from (0) to (10);
-create index concurrently on idxpart (a);
+create table idxpart1 partition of idxpart for values from (0) to (10) partition by range(a);
+create table idxpart11 partition of idxpart1 for values from (0) to (10) partition by range(a);
+create table idxpart111 partition of idxpart11 default partition by range(a);
+create table idxpart1111 partition of idxpart111 default partition by range(a);
+create table idxpart2 partition of idxpart for values from (10) to (20);
+create table idxpart3 partition of idxpart for values from (30) to (40) partition by range(a);
+create table idxpart31 partition of idxpart3 default;
+
+insert into idxpart2 values(10),(10); -- not unique
+create index concurrently on idxpart (a); -- partitioned
+create index concurrently on idxpart1 (a); -- partitioned and partition
+create index concurrently on idxpart11 (a); -- partitioned and partition, with no leaves
+create index concurrently on idxpart2 (a); -- leaf
+create unique index concurrently on idxpart (a); -- partitioned, unique failure
+\d idxpart
+\d idxpart1
+\d idxpart11
+\d idxpart111
+\d idxpart1111
+\d idxpart2
+\d idxpart3
+\d idxpart31
 drop table idxpart;
 
 -- Verify bugfix with query on indexed partitioned table with no partitions
-- 
2.25.1


--aPdhxNJGSeOG9wFI--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v2] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index fb6a4914b06..1da073f0aaa 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index d3febfe58f1..075194a8af8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1844,6 +1844,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index 6d304f32fb0..d346aefe9b6 100644
--- a/meson.build
+++ b/meson.build
@@ -2694,6 +2694,7 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..b41a5f9dcca 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#if HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#if HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#if HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 339268dc8ef..e7b8e023829 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -80,6 +80,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of `memset_s', and to 0 if you
    don't. */
 #undef HAVE_DECL_MEMSET_S

base-commit: 6831cd9e3b082d7b830c3196742dd49e3540c49b
-- 
2.49.0


--7u2oub7w3nl66bkx--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread

* [PATCH v1] Use open file description locks for data directory lockfile
@ 2025-12-18 17:21 Dmitrii Dolgov <9erthalion6@gmail.com>
  0 siblings, 0 replies; 659+ messages in thread

From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)

When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.

To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.

This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:

* Portability issues. Open file description lock was a non-POSIX extension in
  Linux and similar flock is from BSD standard. But looks like everybody agrees
  that such locks make more sense than a typical advisory locks, and
  F_OFD_SETLK made its way into POSIX.1 2024 [1].

* Issues with NFS. The current state of things here looks like this:

  - NFSv3 doesn't implement open file description locks, they're converted to
    advisory locks instead. Advisory locks are subject to namespace isolation,
    meaning that processes in different PID namespaces will not see each other
    advisory lock, and it's still possible to run multiple postgres
    instances on the same data directory.

  - NFSv4 uses a lease system for locking, I haven't found any mention of
    conversion to advisory locks neither in the man page nor in RFC [2].

To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.

[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
---
 configure                         |  14 ++++
 configure.ac                      |   3 +
 meson.build                       |   1 +
 src/backend/utils/init/miscinit.c | 107 +++++++++++++++++++++++++-----
 src/include/pg_config.h.in        |   4 ++
 5 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/configure b/configure
index 14ad0a5006f..b176ac39799 100755
--- a/configure
+++ b/configure
@@ -16177,6 +16177,20 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
 ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
 if test "x$ac_cv_func_explicit_bzero" = xyes; then :
   $as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 01b3bbc1be8..d6cf1f27771 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1838,6 +1838,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
 # This is probably only present on macOS, but may as well check always
 AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
 
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
 AC_REPLACE_FUNCS(m4_normalize([
 	explicit_bzero
 	getopt
diff --git a/meson.build b/meson.build
index d7c5193d4ce..ad9ecad829f 100644
--- a/meson.build
+++ b/meson.build
@@ -2667,6 +2667,7 @@ decl_checks = [
   ['strnlen', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['F_OFD_SETLK', 'fcntl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index fec79992c8d..78bb7df543e 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -68,6 +68,11 @@ static List *lock_files = NIL;
 
 static Latch LocalLatchData;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+/* File descriptor for data directory lock file. */
+static int DataDirLockFD;
+#endif
+
 /* ----------------------------------------------------------------
  *		ignoring system indexes support stuff
  *
@@ -1117,6 +1122,45 @@ RestoreClientConnectionInfo(char *conninfo)
  *-------------------------------------------------------------------------
  */
 
+/*
+ * Flock the data directory lockfile.
+ *
+ * Lock the data directory lockfile with an open file description lock. If the
+ * lock is already taken, it's a hard stop. It's only a best effort test, and
+ * any other errors are ignored. On succes the file descriptor is duplicated,
+ * to make sure there will be at least one open copy of it to keep the lock.
+ *
+ * filename is used only for reporting purposes.
+ */
+static void
+FlockDataDirLockFile(int fd, const char *filename)
+{
+
+#ifdef HAVE_DECL_F_OFD_SETLK
+	struct flock lock;
+
+	lock.l_type		= F_WRLCK;
+	lock.l_whence	= SEEK_SET;
+	lock.l_start	= 0;
+	lock.l_len		= 0;
+	lock.l_pid		= 0;
+
+	if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+	{
+		if (errno == EAGAIN)
+			ereport(FATAL,
+					(errcode(ERRCODE_LOCK_FILE_EXISTS),
+					 errmsg("cannot lock the lock file \"%s\"", filename),
+					 errhint("Another server is starting.")));
+		else
+			elog(WARNING, "Failed locking file \"%s\", %m", filename);
+	}
+	else
+		DataDirLockFD = dup(fd);
+#endif
+
+}
+
 /*
  * proc_exit callback to remove lockfiles.
  */
@@ -1125,6 +1169,11 @@ UnlinkLockFiles(int status, Datum arg)
 {
 	ListCell   *l;
 
+#ifdef HAVE_DECL_F_OFD_SETLK
+	/* Close the file descriptor, which keeps the open file description lock */
+	close(DataDirLockFD);
+#endif
+
 	foreach(l, lock_files)
 	{
 		char	   *curfile = (char *) lfirst(l);
@@ -1171,22 +1220,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
 	const char *envvar;
 
 	/*
-	 * If the PID in the lockfile is our own PID or our parent's or
-	 * grandparent's PID, then the file must be stale (probably left over from
-	 * a previous system boot cycle).  We need to check this because of the
-	 * likelihood that a reboot will assign exactly the same PID as we had in
-	 * the previous reboot, or one that's only one or two counts larger and
-	 * hence the lockfile's PID now refers to an ancestor shell process.  We
-	 * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
-	 * via the environment variable PG_GRANDPARENT_PID; this is so that
-	 * launching the postmaster via pg_ctl can be just as reliable as
-	 * launching it directly.  There is no provision for detecting
-	 * further-removed ancestor processes, but if the init script is written
-	 * carefully then all but the immediate parent shell will be root-owned
-	 * processes and so the kill test will fail with EPERM.  Note that we
-	 * cannot get a false negative this way, because an existing postmaster
-	 * would surely never launch a competing postmaster or pg_ctl process
-	 * directly.
+	 * If we find an already existing lockfile containing our own PID,
+	 * there are few options:
+	 *
+	 * - There is another process, that we don't see due to PID namespace
+	 *   isolation, which is already running in this data directory.
+	 *
+	 *   To prevent two concurrent processes working with the same data
+	 *   directory, we first try to lock the lockfile exclusively.
+	 *
+	 * - The file must be stale, probably left over from a previous system boot
+	 *   cycle. The same if the lockfile contains our parent's or grandparent's
+	 *   PID.
+	 *
+	 *   We need to check this because of the likelihood that a reboot will
+	 *   assign exactly the same PID as we had in the previous reboot, or one
+	 *   that's only one or two counts larger and hence the lockfile's PID now
+	 *   refers to an ancestor shell process.  We allow pg_ctl to pass down its
+	 *   parent shell PID (our grandparent PID) via the environment variable
+	 *   PG_GRANDPARENT_PID; this is so that launching the postmaster via
+	 *   pg_ctl can be just as reliable as launching it directly.  There is no
+	 *   provision for detecting further-removed ancestor processes, but if the
+	 *   init script is written carefully then all but the immediate parent
+	 *   shell will be root-owned processes and so the kill test will fail with
+	 *   EPERM.  Note that we cannot get a false negative this way, because an
+	 *   existing postmaster would surely never launch a competing postmaster
+	 *   or pg_ctl process directly.
 	 */
 	my_pid = getpid();
 
@@ -1222,7 +1281,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		 */
 		fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
 		if (fd >= 0)
-			break;				/* Success; exit the retry loop */
+		{
+			/* Success; lock and exit the retry loop */
+			FlockDataDirLockFile(fd, filename);
+			break;
+		}
 
 		/*
 		 * Couldn't create the pid file. Probably it already exists.
@@ -1236,8 +1299,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
 		/*
 		 * Read the file to get the old owner's PID.  Note race condition
 		 * here: file might have been deleted since we tried to create it.
+		 *
+		 * We're going to use the same fd for flock, and want to create a write
+		 * lock for the latter one. Since both fd and the lock have to be of
+		 * the same type, open the file for read and write.
 		 */
-		fd = open(filename, O_RDONLY, pg_file_create_mode);
+		fd = open(filename, O_RDWR, pg_file_create_mode);
 		if (fd < 0)
 		{
 			if (errno == ENOENT)
@@ -1247,6 +1314,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
 					 errmsg("could not open lock file \"%s\": %m",
 							filename)));
 		}
+
+		/* Try to lock the file. We stop here, if it's already locked. */
+		FlockDataDirLockFile(fd, filename);
+
 		pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
 		if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
 			ereport(FATAL,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 92fcc5f3063..c19a50f108e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -83,6 +83,10 @@
    don't. */
 #undef HAVE_DECL_F_FULLFSYNC
 
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+   don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
 /* Define to 1 if you have the declaration of
    `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */
 #undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER

base-commit: 44f49511b7940adf3be4337d4feb2de38fe92297
-- 
2.49.0


--n6t6hie3zne7u6vd--





^ permalink  raw  reply  [nested|flat] 659+ messages in thread


end of thread, other threads:[~2025-12-18 17:21 UTC | newest]

Thread overview: 659+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2020-06-06 22:42 [PATCH] Allow CREATE INDEX CONCURRENTLY on partitioned table Justin Pryzby <pryzbyj@telsasoft.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v2] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>
2025-12-18 17:21 [PATCH v1] Use open file description locks for data directory lockfile Dmitrii Dolgov <9erthalion6@gmail.com>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox